#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
专业可视化模块
Professional Visualization Module

专业级回测功能升级 - 可视化组件
支持净值曲线、回撤分析、收益分布、风险收益散点图等专业图表

作者: 前端工程师 + 量化分析师
日期: 2025-08-04
"""

import logging
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
from typing import Dict, List, Optional, Any, Tuple
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import matplotlib.dates as mdates
from matplotlib.patches import Rectangle
import warnings

warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

logger = logging.getLogger('户部尚赢.专业回测.可视化')

class ProfessionalVisualizer:
    """专业可视化器"""
    
    def __init__(self, style: str = 'professional'):
        """初始化可视化器"""
        self.style = style
        self.color_palette = {
            'strategy': '#1f77b4',
            'benchmark': '#ff7f0e', 
            'excess': '#2ca02c',
            'drawdown': '#d62728',
            'positive': '#2ca02c',
            'negative': '#d62728',
            'neutral': '#7f7f7f'
        }
        
        # 设置图表样式
        if style == 'professional':
            plt.style.use('seaborn-v0_8-whitegrid')
            sns.set_palette("husl")
        
        logger.info("✅ 专业可视化器初始化完成")
    
    def plot_equity_curve(self, strategy_equity: pd.Series, 
                         benchmark_equity: pd.Series = None,
                         title: str = "策略净值曲线对比") -> go.Figure:
        """绘制净值曲线图"""
        try:
            logger.info("📈 绘制净值曲线图")
            
            fig = go.Figure()
            
            # 策略净值曲线
            fig.add_trace(go.Scatter(
                x=strategy_equity.index,
                y=strategy_equity.values,
                mode='lines',
                name='策略净值',
                line=dict(color=self.color_palette['strategy'], width=2),
                hovertemplate='日期: %{x}<br>净值: %{y:.2f}<extra></extra>'
            ))
            
            # 基准净值曲线
            if benchmark_equity is not None:
                fig.add_trace(go.Scatter(
                    x=benchmark_equity.index,
                    y=benchmark_equity.values,
                    mode='lines',
                    name='基准净值',
                    line=dict(color=self.color_palette['benchmark'], width=2),
                    hovertemplate='日期: %{x}<br>净值: %{y:.2f}<extra></extra>'
                ))
            
            # 图表布局
            fig.update_layout(
                title=dict(text=title, x=0.5, font=dict(size=16)),
                xaxis_title="日期",
                yaxis_title="净值",
                hovermode='x unified',
                template='plotly_white',
                legend=dict(x=0.02, y=0.98),
                height=500
            )
            
            # 添加网格
            fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')
            fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='lightgray')
            
            logger.info("✅ 净值曲线图绘制完成")
            return fig
            
        except Exception as e:
            logger.error(f"❌ 绘制净值曲线图失败: {e}")
            return go.Figure()
    
    def plot_drawdown_analysis(self, equity_curve: pd.Series,
                             title: str = "回撤分析") -> go.Figure:
        """绘制回撤分析图"""
        try:
            logger.info("📉 绘制回撤分析图")
            
            # 计算回撤
            peak = equity_curve.expanding().max()
            drawdown = (equity_curve - peak) / peak * 100
            
            # 创建子图
            fig = make_subplots(
                rows=2, cols=1,
                subplot_titles=('净值曲线', '回撤曲线'),
                vertical_spacing=0.1,
                row_heights=[0.6, 0.4]
            )
            
            # 净值曲线
            fig.add_trace(go.Scatter(
                x=equity_curve.index,
                y=equity_curve.values,
                mode='lines',
                name='净值',
                line=dict(color=self.color_palette['strategy'], width=2)
            ), row=1, col=1)
            
            # 回撤曲线
            fig.add_trace(go.Scatter(
                x=drawdown.index,
                y=drawdown.values,
                mode='lines',
                name='回撤',
                line=dict(color=self.color_palette['drawdown'], width=2),
                fill='tonexty',
                fillcolor='rgba(214, 39, 40, 0.3)'
            ), row=2, col=1)
            
            # 添加零线
            fig.add_hline(y=0, line_dash="dash", line_color="gray", row=2, col=1)
            
            # 布局设置
            fig.update_layout(
                title=dict(text=title, x=0.5, font=dict(size=16)),
                template='plotly_white',
                height=600,
                showlegend=True
            )
            
            fig.update_xaxes(title_text="日期", row=2, col=1)
            fig.update_yaxes(title_text="净值", row=1, col=1)
            fig.update_yaxes(title_text="回撤 (%)", row=2, col=1)
            
            logger.info("✅ 回撤分析图绘制完成")
            return fig
            
        except Exception as e:
            logger.error(f"❌ 绘制回撤分析图失败: {e}")
            return go.Figure()
    
    def plot_return_distribution(self, returns: pd.Series,
                               title: str = "收益率分布分析") -> go.Figure:
        """绘制收益率分布图"""
        try:
            logger.info("📊 绘制收益率分布图")
            
            # 创建子图
            fig = make_subplots(
                rows=2, cols=2,
                subplot_titles=('收益率直方图', '收益率箱线图', 'Q-Q图', '累计分布函数'),
                specs=[[{"secondary_y": False}, {"secondary_y": False}],
                       [{"secondary_y": False}, {"secondary_y": False}]]
            )
            
            # 1. 直方图
            fig.add_trace(go.Histogram(
                x=returns.values,
                nbinsx=50,
                name='收益率分布',
                marker_color=self.color_palette['strategy'],
                opacity=0.7
            ), row=1, col=1)
            
            # 2. 箱线图
            fig.add_trace(go.Box(
                y=returns.values,
                name='收益率',
                marker_color=self.color_palette['strategy']
            ), row=1, col=2)
            
            # 3. Q-Q图（正态分布）
            from scipy import stats
            theoretical_quantiles = stats.norm.ppf(np.linspace(0.01, 0.99, len(returns)))
            sample_quantiles = np.sort(returns.values)
            
            fig.add_trace(go.Scatter(
                x=theoretical_quantiles,
                y=sample_quantiles,
                mode='markers',
                name='Q-Q图',
                marker=dict(color=self.color_palette['strategy'])
            ), row=2, col=1)
            
            # 添加理论线
            min_val = min(theoretical_quantiles.min(), sample_quantiles.min())
            max_val = max(theoretical_quantiles.max(), sample_quantiles.max())
            fig.add_trace(go.Scatter(
                x=[min_val, max_val],
                y=[min_val, max_val],
                mode='lines',
                name='理论线',
                line=dict(color='red', dash='dash')
            ), row=2, col=1)
            
            # 4. 累计分布函数
            sorted_returns = np.sort(returns.values)
            cumulative_prob = np.arange(1, len(sorted_returns) + 1) / len(sorted_returns)
            
            fig.add_trace(go.Scatter(
                x=sorted_returns,
                y=cumulative_prob,
                mode='lines',
                name='经验CDF',
                line=dict(color=self.color_palette['strategy'])
            ), row=2, col=2)
            
            # 布局设置
            fig.update_layout(
                title=dict(text=title, x=0.5, font=dict(size=16)),
                template='plotly_white',
                height=700,
                showlegend=False
            )
            
            logger.info("✅ 收益率分布图绘制完成")
            return fig
            
        except Exception as e:
            logger.error(f"❌ 绘制收益率分布图失败: {e}")
            return go.Figure()
    
    def plot_risk_return_scatter(self, strategies_data: Dict[str, Dict],
                               title: str = "风险收益散点图") -> go.Figure:
        """绘制风险收益散点图"""
        try:
            logger.info("🎯 绘制风险收益散点图")
            
            fig = go.Figure()
            
            # 为每个策略添加散点
            for strategy_name, data in strategies_data.items():
                fig.add_trace(go.Scatter(
                    x=[data['volatility']],
                    y=[data['return']],
                    mode='markers+text',
                    name=strategy_name,
                    text=[strategy_name],
                    textposition="top center",
                    marker=dict(
                        size=data.get('sharpe_ratio', 1) * 20 + 10,  # 根据夏普比率调整大小
                        opacity=0.7
                    ),
                    hovertemplate=f'<b>{strategy_name}</b><br>' +
                                '收益率: %{y:.2%}<br>' +
                                '波动率: %{x:.2%}<br>' +
                                f'夏普比率: {data.get("sharpe_ratio", 0):.3f}<extra></extra>'
                ))
            
            # 添加有效前沿线（简化版）
            if len(strategies_data) > 1:
                volatilities = [data['volatility'] for data in strategies_data.values()]
                returns = [data['return'] for data in strategies_data.values()]
                
                # 简单的有效前沿近似
                vol_range = np.linspace(min(volatilities), max(volatilities), 100)
                efficient_returns = np.interp(vol_range, sorted(volatilities), 
                                            [returns[i] for i in np.argsort(volatilities)])
                
                fig.add_trace(go.Scatter(
                    x=vol_range,
                    y=efficient_returns,
                    mode='lines',
                    name='有效前沿',
                    line=dict(color='gray', dash='dash'),
                    showlegend=False
                ))
            
            # 布局设置
            fig.update_layout(
                title=dict(text=title, x=0.5, font=dict(size=16)),
                xaxis_title="波动率 (年化)",
                yaxis_title="收益率 (年化)",
                template='plotly_white',
                height=500,
                hovermode='closest'
            )
            
            # 格式化坐标轴为百分比
            fig.update_xaxes(tickformat='.1%')
            fig.update_yaxes(tickformat='.1%')
            
            logger.info("✅ 风险收益散点图绘制完成")
            return fig
            
        except Exception as e:
            logger.error(f"❌ 绘制风险收益散点图失败: {e}")
            return go.Figure()
    
    def plot_rolling_metrics(self, rolling_data: pd.DataFrame,
                           title: str = "滚动绩效指标") -> go.Figure:
        """绘制滚动绩效指标"""
        try:
            logger.info("📈 绘制滚动绩效指标")
            
            # 创建子图
            metrics = ['annualized_return', 'volatility', 'sharpe_ratio', 'max_drawdown']
            metric_names = ['年化收益率', '波动率', '夏普比率', '最大回撤']
            
            fig = make_subplots(
                rows=2, cols=2,
                subplot_titles=metric_names,
                vertical_spacing=0.1,
                horizontal_spacing=0.1
            )
            
            positions = [(1, 1), (1, 2), (2, 1), (2, 2)]
            
            for i, (metric, metric_name) in enumerate(zip(metrics, metric_names)):
                if metric in rolling_data.columns:
                    row, col = positions[i]
                    
                    fig.add_trace(go.Scatter(
                        x=rolling_data.index,
                        y=rolling_data[metric],
                        mode='lines',
                        name=metric_name,
                        line=dict(width=2),
                        showlegend=False
                    ), row=row, col=col)
                    
                    # 添加零线（对于可能为负的指标）
                    if metric in ['annualized_return', 'max_drawdown']:
                        fig.add_hline(y=0, line_dash="dash", line_color="gray", 
                                    row=row, col=col)
            
            # 布局设置
            fig.update_layout(
                title=dict(text=title, x=0.5, font=dict(size=16)),
                template='plotly_white',
                height=600
            )
            
            logger.info("✅ 滚动绩效指标图绘制完成")
            return fig
            
        except Exception as e:
            logger.error(f"❌ 绘制滚动绩效指标失败: {e}")
            return go.Figure()
    
    def plot_monthly_returns_heatmap(self, returns: pd.Series,
                                   title: str = "月度收益率热力图") -> go.Figure:
        """绘制月度收益率热力图"""
        try:
            logger.info("🔥 绘制月度收益率热力图")
            
            # 计算月度收益率
            monthly_returns = returns.resample('M').apply(lambda x: (1 + x).prod() - 1)
            
            # 创建年月矩阵
            monthly_returns.index = pd.to_datetime(monthly_returns.index)
            monthly_data = monthly_returns.groupby([
                monthly_returns.index.year,
                monthly_returns.index.month
            ]).first().unstack()
            
            # 月份名称
            month_names = ['1月', '2月', '3月', '4月', '5月', '6月',
                          '7月', '8月', '9月', '10月', '11月', '12月']
            
            # 创建热力图
            fig = go.Figure(data=go.Heatmap(
                z=monthly_data.values,
                x=month_names,
                y=monthly_data.index,
                colorscale='RdYlGn',
                zmid=0,
                text=np.round(monthly_data.values * 100, 2),
                texttemplate='%{text}%',
                textfont={"size": 10},
                hovertemplate='年份: %{y}<br>月份: %{x}<br>收益率: %{z:.2%}<extra></extra>'
            ))
            
            # 布局设置
            fig.update_layout(
                title=dict(text=title, x=0.5, font=dict(size=16)),
                xaxis_title="月份",
                yaxis_title="年份",
                template='plotly_white',
                height=400
            )
            
            logger.info("✅ 月度收益率热力图绘制完成")
            return fig
            
        except Exception as e:
            logger.error(f"❌ 绘制月度收益率热力图失败: {e}")
            return go.Figure()
    
    def create_comprehensive_dashboard(self, strategy_equity: pd.Series,
                                     benchmark_equity: pd.Series = None,
                                     returns: pd.Series = None,
                                     metrics: Dict = None) -> Dict[str, go.Figure]:
        """创建综合仪表板"""
        try:
            logger.info("📊 创建综合仪表板")
            
            dashboard = {}
            
            if returns is None:
                returns = strategy_equity.pct_change().dropna()
            
            # 1. 净值曲线图
            dashboard['equity_curve'] = self.plot_equity_curve(
                strategy_equity, benchmark_equity, "策略净值曲线对比"
            )
            
            # 2. 回撤分析图
            dashboard['drawdown_analysis'] = self.plot_drawdown_analysis(
                strategy_equity, "回撤分析"
            )
            
            # 3. 收益率分布图
            dashboard['return_distribution'] = self.plot_return_distribution(
                returns, "收益率分布分析"
            )
            
            # 4. 月度收益率热力图
            dashboard['monthly_heatmap'] = self.plot_monthly_returns_heatmap(
                returns, "月度收益率热力图"
            )
            
            # 5. 如果有多个策略数据，创建风险收益散点图
            if metrics and isinstance(metrics, dict):
                strategies_data = {}
                for name, metric_dict in metrics.items():
                    strategies_data[name] = {
                        'return': metric_dict.get('annualized_return', 0),
                        'volatility': metric_dict.get('volatility', 0),
                        'sharpe_ratio': metric_dict.get('sharpe_ratio', 0)
                    }
                
                if len(strategies_data) > 1:
                    dashboard['risk_return_scatter'] = self.plot_risk_return_scatter(
                        strategies_data, "风险收益散点图"
                    )
            
            logger.info(f"✅ 综合仪表板创建完成，包含 {len(dashboard)} 个图表")
            return dashboard
            
        except Exception as e:
            logger.error(f"❌ 创建综合仪表板失败: {e}")
            return {}
    
    def save_figures_to_html(self, figures: Dict[str, go.Figure], 
                           output_path: str = "backtest_report.html"):
        """保存图表到HTML文件"""
        try:
            logger.info(f"💾 保存图表到HTML文件: {output_path}")
            
            html_content = """
            <!DOCTYPE html>
            <html>
            <head>
                <title>专业回测分析报告</title>
                <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
                <style>
                    body { font-family: Arial, sans-serif; margin: 20px; }
                    .chart-container { margin-bottom: 30px; }
                    h1 { color: #333; text-align: center; }
                    h2 { color: #666; border-bottom: 2px solid #ddd; padding-bottom: 10px; }
                </style>
            </head>
            <body>
                <h1>专业回测分析报告</h1>
            """
            
            for title, fig in figures.items():
                html_content += f'<div class="chart-container"><h2>{title}</h2>'
                html_content += fig.to_html(include_plotlyjs=False, div_id=f"chart_{title}")
                html_content += '</div>'
            
            html_content += """
            </body>
            </html>
            """
            
            with open(output_path, 'w', encoding='utf-8') as f:
                f.write(html_content)
            
            logger.info(f"✅ HTML报告保存完成: {output_path}")
            
        except Exception as e:
            logger.error(f"❌ 保存HTML报告失败: {e}")

# 使用示例
if __name__ == "__main__":
    # 创建可视化器
    visualizer = ProfessionalVisualizer()
    
    # 生成模拟数据
    dates = pd.date_range('2024-01-01', '2024-12-31', freq='D')
    strategy_returns = np.random.normal(0.001, 0.02, len(dates))
    benchmark_returns = np.random.normal(0.0005, 0.015, len(dates))
    
    strategy_equity = pd.Series(1000000 * (1 + strategy_returns).cumprod(), index=dates)
    benchmark_equity = pd.Series(1000000 * (1 + benchmark_returns).cumprod(), index=dates)
    returns = pd.Series(strategy_returns, index=dates)
    
    # 创建综合仪表板
    dashboard = visualizer.create_comprehensive_dashboard(
        strategy_equity, benchmark_equity, returns
    )
    
    # 保存到HTML文件
    visualizer.save_figures_to_html(dashboard, "专业回测分析报告.html")
    
    print("✅ 专业可视化示例完成")
    print(f"生成图表数量: {len(dashboard)}")
    print("HTML报告已保存: 专业回测分析报告.html")
